Swiftで簡単なAPIサーバーを作ってみた(Vapor編)
前回、サーバーサイドSwiftのフレームワークVaporを使用してHello, world!
と出力するところまで試してみました。
今回はAPIサーバーっぽく、データをPOSTしたりGETしたり出来る様にしていきたいと思います。
Vaporについては前回の記事で触れている箇所も多いので、そちらをご覧いただければと思います。
環境
- Xcode 14.1
- Vapor 4
Vaporプロジェクトの作成
ターミナルで任意のディレクトリに移動します。
cd ~/desktop
新しいVaporプロジェクトを作成します。your-project-name
には任意の名前を入力します。今回、lilossa-coordinate
という名前にしました。
vapor new your-project-name
Fluentフレームワークを使用するか問われます。
Cloning template... name: hello-world Would you like to use Fluent? (--fluent/--no-fluent) y/n>
今回はFluentフレームワークを使用するのでy
を入力します。
使用するデータベースを問われるので推奨されているPostgressを選択します。
Cloning template... name: lilossa-coordinate Would you like to use Fluent? (--fluent/--no-fluent) y/n> y fluent: Yes Which database would you like to use? (--fluent.db) 1: Postgres (Recommended) 2: MYSQL 3: SQLite 4: Mongo
次にLeafフレームワークを使用するか問われます。
Would you like to use Leaf? (--leaf/--no-leaf) y/n>
今回は使用しない為、n
を入力します。
アスキーアートのようなものが表示されたら、新しいVaporプロジェクトの作成は完了です。
データベースの作成
今回はPostgresを使用するのでアプリケーションをダウンロードします。
こちらのページから無料でダウンロードを行えます。
Postgresのダウンロードが出来たら、Postgresを起動します。
最初は1つのみデータベースが表示されていると思うので、その1つをダブルクリックします。
ターミナルが立ち上がります。
CREATE DATABASE
と入力します。大文字でも小文字で問題ありません。その後ろに任意のデータベースを入力します。末尾のセミコロンは必須なので忘れないようにしましょう。
CREATE DATABASE yourdatabasename;
入力後、CREATE DATABASE
と表示されれば完了です。
Vaporプロジェクトの設定
データベースの作成は出来たので、Vaporプロジェクト側の設定を進めていきます。
Modelの作成
作成したVaporプロジェクトのSource > App > Models フォルダ内で新規ファイルを追加します。デフォルトでTODO
用のモデル等が存在していますが、そちらは削除して問題ありません。
今回は、LilossaCoordinate
というModelを作成しました。
import Fluent import Vapor final class LilossaCoordinate: Model, Content { static let schema = "littleossaCoordinates" enum DataField: String { case createdAt = "created_at" case latitude case longitude var key: FieldKey { return FieldKey(stringLiteral: self.rawValue) } } @ID(key: .id) var id: UUID? /// 日付 @Timestamp(key: DataField.createdAt.key, on: .create, format: .iso8601) var createdAt: Date? /// 緯度 @Field(key: DataField.latitude.key) var latitude: Double /// 経度 @Field(key: DataField.longitude.key) var longitude: Double init() { } init(id: UUID? = nil, latitude: Double, longitude: Double) { self.id = id self.latitude = latitude self.longitude = longitude } }
- id
- 一意のid
- createdAt
- モデルの作成日
- latitude
- 緯度
- longitude
- 経度
schema
モデルをデータベース上で使用する為に任意の文字列を設定します。
@ID(key:)
@IDを付与することで、自動で一意のIDが付与されます。keyは、こちらで値を設定する必要もない為、今回は.id
を指定しました。
@Timestamp(key:, on:, format:)
@Timestampを付与することで、選択したトリガーにしたがって自動に付与されます。今回は作成日をトリガーに値を付与したかったのでon: .create
を指定しています。
その他のトリガーについてはドキュメントで確認出来ます。
key
にはFieldKey
型の任意のキー値を入力します。
@Field(key:)
@Fieldを付与することで、一般的なキー値で検索可能な値を作成することが出来ます。key
には任意のキー値を入力します。
マイグレーションファイルを作成
まだデータベース上にテーブルが作成されていない為、マイグレーションファイルを作成し、データベースとマイグレーションを行います。
Source > App > Migrationsフォルダ内で新規ファイルを作成します。今回はCreateCoordinate
というファイルを作成しました。
import Fluent struct CreateCoordinate: AsyncMigration { // データテーブルを用意 func prepare(on database: Database) async throws { try await database.schema(LilossaCoordinate.schema) // Table name .id() .field(LilossaCoordinate.DataField.createdAt.key, .string) // Column name 日付 .field(LilossaCoordinate.DataField.latitude.key, .double, .required) // Column name 緯度 .field(LilossaCoordinate.DataField.longitude.key, .double, .required) // Column name 経度 .create() } // データテーブルを元に戻す func revert(on database: Database) async throws { try await database.schema(LilossaCoordinate.schema).delete() } }
作成したLilossaCoordinate
をマイグレーションする為に、データベースのスキーマ、フィールドにキー値を設定します。
.field(LilossaCoordinate.DataField.createdAt.key, .string)
は、モデル側では@Timestamp
でDate?
型を設定しているのですが、マイグレーションの際には.string
を指定する必要があります。
.field
field(_ key: FieldKey, _ dataType: DatabaseSchema.DataType, _ constraints: DatabaseSchema.FieldConstraint...) -> Self
.filed
には、キー値、データタイプと制約を指定出来ます。
今回のデータタイプは.double
、制約はnil
を許容しない.required
を指定していますが、その他のタイプについてはドキュメントに詳しく記載があります。
.create
今回のマイグレーションは作成時のマイグレーションなので.create()
を呼び出します。更新時のマイグレーションは、.update()
を呼び出します。
マイグレーションをconfigureに追加
マイグレーションファイルをconfigure
に追加します。
Source > App > configureファイルを変更します。
import Fluent import FluentPostgresDriver import Vapor // configures your application public func configure(_ app: Application) throws { app.databases.use(.postgres( hostname: "localhost", username: "postgres", password: "", database: "littleossacoordinatedb" ), as: .psql) // マイグレーションの実行 app.migrations.add(CreateCoordinate()) // register routes try routes(app) }
データベースを指定
app.databases.use
で使用するデータベースを指定します。
hostname
はローカルホストを使用するので、localhost
username
とpassword
については今回は変更していないのでデフォルト値を設定します。
database
は作成したデータベース名を設定します。
マイグレーションを追加
作成したマイグレーションを追加します。
app.migrations.add(CreateCoordinate())
以上でマイグレーションの準備は整いました。
マイグレーションを実行
Vaporディレクトリに移動しているターミナルで下記を実行
vapor run migrate
Would you like to continue?と問われるので、y
を選択し、Migration successful
と表示されるとマイグレーションが完了です。
y/n> y [ INFO ] [Migrator] Starting prepare [database-id: psql, migration: App.CreateCoordinate] [ INFO ] [Migrator] Finished prepare [database-id: psql, migration: App.CreateCoordinate] Migration successful
Controllerの実装
routesに関する処理を行うControllerを作成します。
まず、Source > App > Controllerに、新規ファイルを追加します。デフォルトでTodoController
が存在していますので、そちらを参考にしながら処理を書き進めると良さそうです。
今回は、LilossaCoordinateController
という名前で作成しました。
import Fluent import Vapor struct LilossaCoordinateController: RouteCollection { func boot(routes: Vapor.RoutesBuilder) throws { /// localhost:8080/littleossa_coordinates let coordinates = routes.grouped(PathComponent(stringLiteral: LilossaCoordinate.schema)) // GET coordinates.get(use: index) // POST coordinates.post(use: create) // DELETE /// localhost:8080/littleossa_coordinates/:coordinateID coordinates.group(":coordinateID") { coordinate in coordinate.delete(use: delete) } } func index(req: Request) async throws -> [LilossaCoordinate] { try await LilossaCoordinate.query(on: req.db) .sort(\.$createdAt, .descending) // 生成日の降順に並び替え .all() } func create(req: Request) async throws -> LilossaCoordinate { let coordinate = try req.content.decode(LilossaCoordinate.self) try await coordinate.save(on: req.db) return coordinate } func delete(req: Request) async throws -> HTTPStatus { guard let coordinate = try await LilossaCoordinate.find(req.parameters.get("coordinateID"), on: req.db) else { throw Abort(.notFound) } try await coordinate.delete(on: req.db) return .noContent } }
boot(routes:)
RouteCollection
に準拠すると、boot(routes:)
メソッドを追加する必要があり、そのメソッドの中で細かいroutesの処理を書いていきます。
今回は例にならって、GET、POST、DELETEの3つの処理を追加しました。
index(req:)
boot(route:)
内でcoordinates.get(use: index)
と呼んでいるGETメソッドの処理です。
データベースからLilossaCoordinate
モデルを取得して、生成日の降順に並び替えた全ての要素を取得するものになります。
func index(req: Request) async throws -> [LilossaCoordinate] { try await LilossaCoordinate.query(on: req.db) .sort(\.$createdAt, .descending) // 生成日の降順に並び替え .all() }
create(req:)
boot(route:)
内でcoordinates.post(use: create)
と呼んでいるPOSTメソッドの処理です。
受け取ったリクエストからLilossaCoordinate
を生成して、データベースに保存しています。
func create(req: Request) async throws -> LilossaCoordinate { let coordinate = try req.content.decode(LilossaCoordinate.self) try await coordinate.save(on: req.db) return coordinate }
delete(req:)
boot(route:)
内でcoordinates.delete(use: delete)
と呼んでいるDELETEメソッドの処理です。
littleossa_coordinates
後ろのパスをIDとして受け取ります。
/// localhost:8080/littleossa_coordinates/:coordinateID coordinates.group(":coordinateID") { coordinate in coordinate.delete(use: delete) }
受け取ったIDを使用して、対象のLilossaCoordinate
を見つけ、そのLilossaCoordinate
をデータベースから削除しています。
func delete(req: Request) async throws -> HTTPStatus { guard let coordinate = try await LilossaCoordinate.find(req.parameters.get("coordinateID"), on: req.db) else { throw Abort(.notFound) } try await coordinate.delete(on: req.db) return .noContent }
routesにControllerを追加
Source > Appにあるroutesファイルに作成したControllerを追加します。
import Fluent import Vapor func routes(_ app: Application) throws { try app.register(collection: LilossaCoordinateController()) }
動作確認
サーバーの起動
Vaporプロジェクトで作成したアプリを起動します。
デバッグコンソールにサーバーが起動されたことが表示されます。
[ NOTICE ] Server starting on http://127.0.0.1:8080
Postmanで確認
動作確認には、Postmanを使用しました。Postmanは、APIを構築して使用するためのAPIプラットフォームで、APIの動きを確認するのに便利です。
POST
Postman画面でPOSTを選択し、URLlocalhost:8080/littleossa_coordinates
を入力します。
ヘッダーのKeyにContent-Type
、valueにapplication/json
を入力します。
任意の緯度経度をBodyに入力し、Sendボタンを押すと、コンソール下部に200
ステータスと共に登録されたLilossaCoordinate
の値が確認出来ます。
GET
実際にPOSTしたものが保存されているか確認します。
Postman画面でGETを選択し、URLlocalhost:8080/littleossa_coordinates
を入力します。
特にBodyは不要な為、Sendボタンを押します。
GETで取得した配列内にPOSTで追加したLilossaCoordinate
を確認出来ました。
DELETE
Postman画面でDELETEを選択します。今回は、削除する対象IDが必要な為、URLlocalhost:8080/littleossa_coordinates/853839CB-B2FF-4EDD-B4D2-51610725D5B2
を入力します。
特にBodyは不要な為、Sendボタンを押します。
削除された戻り値204
のNo Content
を確認出来ました。
また、再度GETを実行してみると、空の配列が返ってくるのが確認出来ました。
おわりに
コードはGitHubに置いておきます。
まだ試していないhttpメソッドはあるものの、データベースを更新したり、値を取得、削除することが出来ました。普段から使用しているSwiftを使用出来たことで、触ったことのサーバーサイド側の勉強も負担なく出来たように感じます。まだまだサーバーサイドも入門編なのでこれからも少しずつ触れて仲良くなっていきたいと思います。